# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# The file has been adapted from DeepSeek DeepGEMM project
# Copyright (c) 2025 DeepSeek
# Licensed under the MIT License - https://github.com/deepseek-ai/DeepGEMM/blob/main/LICENSE
"""Utilities"""
import os
import sys

import paddle


def bench(fn, num_warmups: int = 5, num_tests: int = 10, high_precision: bool = False):
    """Benchmark function `fn` using CUDA events."""
    # Flush L2 cache with 256 MB data
    paddle.device.cuda.synchronize()
    cache = paddle.empty(int(256e6 // 4), dtype=paddle.int32)
    cache.zero_()

    # Warmup
    for _ in range(num_warmups):
        fn()

    # Add a large kernel to eliminate the CPU launch overhead
    if high_precision:
        x = paddle.randn((8192, 8192), dtype=paddle.float32)
        y = paddle.randn((8192, 8192), dtype=paddle.float32)
        x @ y

    # Testing
    start_event = paddle.device.cuda.Event(enable_timing=True)
    end_event = paddle.device.cuda.Event(enable_timing=True)
    start_event.record()
    for i in range(num_tests):
        fn()
    end_event.record()
    paddle.cuda.synchronize()

    return start_event.elapsed_time(end_event) / num_tests


def get_cuda_home():
    """Get Cuda home directory"""
    cuda_home = os.environ.get("CUDA_HOME") or os.environ.get("CUDA_PATH")
    if cuda_home:
        return cuda_home

    try:
        which_cmd = "which nvcc"

        nvcc_path = os.popen(which_cmd).read().strip()
        if nvcc_path:
            return os.path.dirname(os.path.dirname(nvcc_path))
    except Exception:
        pass

    return None


class EmptySuppress:
    """Empty context manager"""
    def __enter__(self):
        """Empty context manager"""
        return self

    def __exit__(self, *_):
        """Empty exit method"""
        pass


class SuppressStdoutStderr:
    """Context manager that redirects stdout and stderr"""
    def __enter__(self):
        """Suppress stdout and stderr"""
        self.outnull_file = open(os.devnull, "w")
        self.errnull_file = open(os.devnull, "w")

        self.old_stdout_fileno_undup = sys.stdout.fileno()
        self.old_stderr_fileno_undup = sys.stderr.fileno()

        self.old_stdout_fileno = os.dup(sys.stdout.fileno())
        self.old_stderr_fileno = os.dup(sys.stderr.fileno())

        self.old_stdout = sys.stdout
        self.old_stderr = sys.stderr

        os.dup2(self.outnull_file.fileno(), self.old_stdout_fileno_undup)
        os.dup2(self.errnull_file.fileno(), self.old_stderr_fileno_undup)

        sys.stdout = self.outnull_file
        sys.stderr = self.errnull_file
        return self

    def __exit__(self, *_):
        """Restore stdout and stderr"""
        sys.stdout = self.old_stdout
        sys.stderr = self.old_stderr

        os.dup2(self.old_stdout_fileno, self.old_stdout_fileno_undup)
        os.dup2(self.old_stderr_fileno, self.old_stderr_fileno_undup)

        os.close(self.old_stdout_fileno)
        os.close(self.old_stderr_fileno)

        self.outnull_file.close()
        self.errnull_file.close()


def calc_diff(x, y):
    """Calculate difference between two vectors"""
    x, y = x.astype(paddle.float64), y.astype(paddle.float64)
    denominator = (x * x + y * y).sum()
    sim = 2 * (x * y).sum() / denominator
    return 1 - sim


def count_bytes(tensors):
    """Count number of bytes used by tensors"""
    total = 0
    for t in tensors:
        if isinstance(t, tuple):
            total += count_bytes(t)
        else:
            total += t.numel() * t.element_size()
    return total
